﻿using log4net;
using SharpML.Api.Annotations;
using SharpML.Api.Enumerations;
using SharpML.Api.Extensions;
using SharpML.Api.Interfaces;
using SharpML.Api.Types;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;

namespace SharpML.Api.Implementation
{
    public static class PluginHost
    {
        #region - Static Member Variables -

            /// <summary>
            /// Simple logging pattern
            /// </summary>
            private static readonly ILog Log = LogManager.GetLogger( System.Reflection.MethodBase.GetCurrentMethod().DeclaringType );

            /// <summary>
            /// All the enabled plugin components
            /// </summary>
            public static PluginComponents PluginComponents { get; private set; }

            /// <summary>
            /// All the disabled plugin components
            /// </summary>
            public static PluginComponents DisabledPluginComponents { get; private set; }

            /// <summary>
            /// Contains the enabled plugins
            /// </summary>
            public static IList<PluginInstance> Plugins { get; private set; }

            /// <summary>
            /// Contains the disabled Plugins
            /// </summary>
            public static IList<string> DisabledPlugins { get; private set; }

            /// <summary>
            /// Consumer must switch to GUI thread
            /// </summary>
            public static event Action OnRenderFromConfig = delegate { };

            /// <summary>
            /// Consumer must switch to GUI thread
            /// </summary>
            public static event Action OnStartingEngine = delegate { };

            /// <summary>
            /// Consumer must switch to GUI thread
            /// </summary>
            public static event Action OnEndingEngine = delegate { };

        #endregion - Static Member Variables -

        #region - Static Constructor -

            static PluginHost()
            {
                DisabledPlugins = new List<string>();
            }

        #endregion - Static Constructor -

        #region - Public Functionality -

            /// <summary>
            /// Load all the plugins without further processing
            /// </summary>
            public static void LoadPlugins( string pattern = null, string folderPath = null )
            {
                // Initialize
                Plugins = new List<PluginInstance>();

                // Load all the things!
                foreach( var path in Directory.GetFiles( Path.GetDirectoryName( folderPath ?? Assembly.GetExecutingAssembly().Location ), pattern ?? "*.dll" ) )
                {
                    try
                    {
                        // Make a plugin out of assembly
                        var plugin = new PluginInstance( path );

                        // Is this plugin component-less?
                        if( !plugin.ComponentTypes.Any() )

                            // Ignore it
                            Log.WarnFormat( "Ignoring Assembly: " + Path.GetFileName( path ) );

                        // Otherwise iterate the plugin component types from the plugin
                        else
                        {
                            // Log it!
                            Log.Info( "Plugin Loaded: " + plugin.Name );

                            // Store it!
                            Plugins.Add( plugin );
                        }
                    }
                    catch( Exception ex )
                    {
                        // Log it!
                        Log.Error( "Assembly Failed to Load: " + Path.GetFileName( path ), ex );
                    }
                }
            }

            /// <summary>
            /// Process each loaded plugin, loading the plugin components
            /// </summary>
            public static void ProcessPlugins()
            {
                // No unmanaged resources, ignore disposal and let GC do the work
                // Create a new collection to hold plugin components
                PluginComponents = new PluginComponents();
                DisabledPluginComponents = new PluginComponents();

                // Process each instance
                foreach( var plugin in Plugins )
                    if( !DisabledPlugins.Contains( plugin.Name ) )
                        ProcessPluginInstance( plugin );

                // Order the components incase they're being displayed
                foreach( PluginComponentType componentType in Enum.GetValues( typeof( PluginComponentType ) ) )
                    PluginComponents[componentType] = PluginComponents[componentType].OrderBy( item => item.Name ).ToList();
            }

            /// <summary>
            /// Fires OnRenderFromConfig event
            /// </summary>
            public static void PublishPluginComponents()
            {
                OnRenderFromConfig();
            }

            /// <summary>
            /// Fires the OnStartingEngine event
            /// </summary>
            public static void EnterEngineState()
            {
                Log.InfoFormat( "Entering Engine Mode" );
                OnStartingEngine();
            }

            /// <summary>
            /// Fires the LeaveEngineState event
            /// </summary>
            public static void LeaveEngineState()
            {
                OnEndingEngine();
                Log.InfoFormat( "Left Engine Mode" );
            }

            /// <summary>
            /// Finds a plugin component by type and name, and enable or disables it
            /// </summary>
            /// <param name="pluginComponentType"></param>
            /// <param name="name"></param>
            public static void TogglePluginComponent( Api.Enumerations.PluginComponentType pluginComponentType, string name )
            {
                // Locals
                IEnumerable<IPluginComponent> pluginComponents = PluginComponents[pluginComponentType];

                // Is there a component?
                var pluginComponent = pluginComponents.FirstOrDefault( item => item.GetType().GetDescriptionFromAttribute() == name );

                // Is it enabled?
                if( pluginComponent == null )

                    // Enable it
                    DoEnable( pluginComponentType, DisabledPluginComponents[pluginComponentType].FirstOrDefault( item => item.GetType().GetDescriptionFromAttribute() == name ) );

                // Otherwise disable it
                else DoDisable( pluginComponentType, pluginComponent );

                // Async refresh the UI
                ( (Action)PluginHost.PublishPluginComponents ).BeginInvoke( null, null );
            }

            /// <summary>
            /// Finds a plugin component by type and name, and ensures it's enabled
            /// </summary>
            /// <param name="pluginComponentType"></param>
            /// <param name="name"></param>
            public static void EnablePluginComponent( Api.Enumerations.PluginComponentType pluginComponentType, string name )
            {
                // Locals
                IEnumerable<IPluginComponent> pluginComponents = PluginComponents[pluginComponentType];

                // Is there a component?
                var pluginComponent = pluginComponents.FirstOrDefault( item => item.GetType().GetDescriptionFromAttribute() == name );

                if( pluginComponent == null )
                    DoEnable( pluginComponentType, DisabledPluginComponents[pluginComponentType].FirstOrDefault( item => item.GetType().GetDescriptionFromAttribute() == name ) );

                // Async refresh the UI
                ( (Action)PluginHost.PublishPluginComponents ).BeginInvoke( null, null );
            }

            /// <summary>
            /// Finds a plugin component by type and name, and ensures it's disabled
            /// </summary>
            /// <param name="pluginComponentType"></param>
            /// <param name="name"></param>
            public static void DisablePluginComponent( Api.Enumerations.PluginComponentType pluginComponentType, string name )
            {
                // Locals
                IEnumerable<IPluginComponent> pluginComponents = PluginComponents[pluginComponentType];

                // Is there a component?
                var pluginComponent = pluginComponents.FirstOrDefault( item => item.GetType().GetDescriptionFromAttribute() == name );

                if( pluginComponent != null )
                    DoDisable( pluginComponentType, pluginComponent );

                // Async refresh the UI
                ( (Action)PluginHost.PublishPluginComponents ).BeginInvoke( null, null );
            }

            public static List<PluginConfiguration> PluginConfiguration
            {
                get
                {
                    var results = new List<PluginConfiguration>();

                    foreach( var plugin in PluginComponents.Union( DisabledPluginComponents ).SelectMany( item => item.Value ) )
                        results.Add( new PluginConfiguration { Plugin = plugin.GetType().FullName, Data = plugin.Settings } );

                    return results;
                }
                set
                {
                    foreach( var plugin in PluginComponents.Union( DisabledPluginComponents ).SelectMany( item => item.Value ) )
                    {
                        var settings = value.FirstOrDefault( datum => datum.Plugin == plugin.GetType().FullName );

                        if( settings != null )
                            plugin.Settings = settings.Data;
                    }
                }
            }


        #endregion - Public Functionality -

        #region - Private Static Functionality -

            private static void ProcessPluginInstance( PluginInstance plugin )
        {
            // Load all components from it!
            foreach( var componentType in plugin.ComponentTypes.OrderBy( item => item.Name ) )
                // If this type should be loaded
                if( !componentType.HasAnnotation( typeof( AbstractPluginComponent ) ) )
                    // Catch instantiation exceptions
                    try
                    {
                        // Construct the factory
                        IPluginComponent pluginComponent = Activator.CreateInstance( componentType ) as IPluginComponent;

                        // Sanity check
                        if( pluginComponent == null )

                            // Throw an error just as obscure, but at least a null ref wont show up
                            // in log if they try to break this. It is a demo app after all.
                            throw new InvalidDataException();

                        // If this is already in the collection
                        if( PluginComponents[pluginComponent.Classification].Any( item => item.Name == pluginComponent.Name ) )

                            // Log a warning
                            Log.WarnFormat( "Did not load duplicate component [{0}] from [{1}]", pluginComponent.Name, plugin.Name );

                        // Store component in master collection
                        else PluginComponents[pluginComponent.Classification].Add( pluginComponent );

                        // Is this a factory? ('is' with direct cast is still faster than 'as')
                        if( pluginComponent is IPluginComponentFactory )

                            // Iterate the factory
                            foreach( var subPluginComponent in ( (IPluginComponentFactory)pluginComponent ).GetPluginComponents() )

                                // Add to plugin component collection
                                if( !PluginHost.PluginComponents[subPluginComponent.Classification].Any( item => item.Name == subPluginComponent.Name ) )
                                    PluginHost.PluginComponents[subPluginComponent.Classification].Add( subPluginComponent );
                    }
                    catch( Exception ex )
                    {
                        // Just log it and move on
                        Log.Error( "Failed to load component " + componentType.FullName, ex );
                    }
        }

            /// <summary>
            /// Worker function for disabling a component
            /// </summary>
            /// <param name="pluginComponentType"></param>
            /// <param name="pluginComponent"></param>
            private static void DoDisable( Api.Enumerations.PluginComponentType pluginComponentType, IPluginComponent pluginComponent )
            {
                PluginComponents[pluginComponentType].Remove( pluginComponent );
                DisabledPluginComponents[pluginComponentType].Add( pluginComponent );
            }

            /// <summary>
            /// Worker function for enabling a component
            /// </summary>
            /// <param name="pluginComponentType"></param>
            /// <param name="pluginComponent"></param>
            private static void DoEnable( Api.Enumerations.PluginComponentType pluginComponentType, IPluginComponent pluginComponent )
            {
                DisabledPluginComponents[pluginComponentType].Remove( pluginComponent );
                PluginComponents[pluginComponentType].Add( pluginComponent );
            }

        #endregion - Private Static Functionality -
    }
}